windows 内核回调

sky123

https://www.vergiliusproject.com/

系统通知型回调(Notify Callbacks)

系统通知型回调是 Windows 内核提供的一类机制,允许内核模式驱动程序注册回调函数,以便在系统关键事件(如进程/线程创建、模块加载、句柄操作、注册表修改等)发生时收到通知。

常见的系统通知型回调有下面几种类型:

事件类型 注册函数 回调函数类型
进程创建/退出 PsSetCreateProcessNotifyRoutine(Ex) PCREATE_PROCESS_NOTIFY_ROUTINE(_EX)
线程创建/退出 PsSetCreateThreadNotifyRoutine(Ex) PCREATE_THREAD_NOTIFY_ROUTINE
模块加载(DLL、EXE、SYS) PsSetLoadImageNotifyRoutine(Ex) PLOAD_IMAGE_NOTIFY_ROUTINE
注册表修改 CmRegisterCallbackEx PEX_CALLBACK_FUNCTION
对象句柄操作 ObRegisterCallbacks POB_PRE_OPERATION_CALLBACK
文件系统操作 FltRegisterFilter + FltStartFiltering FLT_PREOP_CALLBACK_ROUTINE
注册表配置单元加载(Hive) CmRegisterCallbackEx PEX_CALLBACK_FUNCTION

进程回调

进程回调是 Windows 内核为驱动提供的重要机制之一,它允许驱动在进程创建和退出时收到系统通知,以实现行为监控、日志记录、安全检测和控制等功能。

Windows 提供两种进程回调机制:

  • PCREATE_PROCESS_NOTIFY_ROUTINE(基础版)
  • PCREATE_PROCESS_NOTIFY_ROUTINE_EX(扩展版,推荐)

基础回调注册

PCREATE_PROCESS_NOTIFY_ROUTINE 是 Windows 早期版本提供的进程回调函数类型,功能相对有限,仅提供进程的 PID、父进程 ID 和创建/销毁事件标志。

1
2
3
4
5
typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE)(
_In_ HANDLE ParentId, // 父进程 PID
_In_ HANDLE ProcessId, // 当前进程 PID
_In_ BOOLEAN Create // TRUE 表示创建进程,FALSE 表示退出进程
);

PCREATE_PROCESS_NOTIFY_ROUTINE 类型的回调需要通过 PsSetCreateProcessNotifyRoutine 函数注册移除

1
2
3
4
5
6
NTKERNELAPI
NTSTATUS
PsSetCreateProcessNotifyRoutine(
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, // 回调函数,当进程创建或销毁时会被调用
_In_ BOOLEAN Remove // 布尔值,TRUE 表示移除回调函数,FALSE 表示注册回调
);

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
VOID BasicProcessNotify(HANDLE ParentId, HANDLE ProcessId, BOOLEAN Create)
{
if (Create) {
DbgPrint("创建进程:PID=%p,父PID=%p\n", ProcessId, ParentId);
} else {
DbgPrint("退出进程:PID=%p\n", ProcessId);
}
}

PsSetCreateProcessNotifyRoutine(BasicProcessNotify, FALSE); // 注册
PsSetCreateProcessNotifyRoutine(BasicProcessNotify, TRUE); // 卸载时移除

注意

如果我们注册了回调函数,但是在驱动卸载的时候没有移除回调函数,则由于实现回调函数的驱动被卸载了,因此会导致内核访问无效内存地址(PAGE_FAULT_IN_NONPAGED_AREA),引发蓝屏崩溃。

因此我们通常在驱动卸载函数中移除回调函数。而 PsSetCreateProcessNotifyRoutine 本身支持注册与移除(通过 Remove=TRUE)。

1
2
3
4
5
6
7
8
9
// 驱动卸载函数 DriverUnload
NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject) {
// 关键:移除回调(第二个参数TRUE表示移除)
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx(MyProcessCallback, TRUE);
if (!NT_SUCCESS(status)) {
KdPrint(("移除回调失败: 0x%X\n", status));
}
return STATUS_SUCCESS;
}

扩展回调注册

PCREATE_PROCESS_NOTIFY_ROUTINE_EX 是对基础回调的扩展,提供更丰富的进程创建上下文,包括 EPROCESS 指针、映像路径、命令行、映像对象等。该回调函数类型定义如下:

1
2
3
4
5
typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE_EX)(
_Inout_ PEPROCESS Process, // 进程对象的指针,包含详细的进程信息
_In_ HANDLE ProcessId, // 目标进程的 ID
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo // 额外的创建通知信息(如命令行等)
);

提示

PCREATE_PROCESS_NOTIFY_ROUTINE_EX 函数没有专门的参数来表示进程是被创建,但是 CreateInfo 参数只有进程创建的时候才非空,因此我们可以通过 CreateInfo 是否为 NULL 来确定进程是销毁还是创建。

  • 当进程正在创建时,CreateInfo != NULL
  • 当进程正在退出时,CreateInfo == NULL

PCREATE_PROCESS_NOTIFY_ROUTINE_EX 不仅提供了进程 ID,还提供了进程的 EPROCESS 结构以及存放额外的信息的结构体 PPS_CREATE_NOTIFY_INFO,该结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _PS_CREATE_NOTIFY_INFO {
_In_ SIZE_T Size; // 0x00: 结构体大小,必须初始化为 sizeof(PS_CREATE_NOTIFY_INFO)
union {
_In_ ULONG Flags; // 0x04: 进程创建特性标志位集合
struct {
_In_ ULONG FileOpenNameAvailable : 1; // bit0: 当设置为1时表示ImageFileName字段包含有效的映像文件名
_In_ ULONG IsSubsystemProcess : 1; // bit1: 当设置为1时表示该进程是子系统进程(如Win32子系统进程)
_In_ ULONG Reserved : 30; // bit2-31: 保留位,必须设置为0,供系统将来使用
};
};
_In_ HANDLE ParentProcessId; // 0x08: 父进程的进程ID(PID),标识创建此进程的父进程
_In_ CLIENT_ID CreatingThreadId; // 0x0C: 创建此进程的线程ID(包含进程ID和线程ID的结构体)
_Inout_ struct _FILE_OBJECT *FileObject; // 0x14: 指向进程映像文件对象的指针,可用于文件重定向
_In_ PCUNICODE_STRING ImageFileName; // 0x18: 指向进程映像文件完整路径的指针(NT路径格式)
_In_opt_ PCUNICODE_STRING CommandLine; // 0x1C: 指向进程命令行参数的指针(可选字段,在较新版本的 Windows 10/11 上才存在,低版本系统或某些子系统进程(如 csrss.exe)中会为 NULL)
_Inout_ NTSTATUS CreationStatus; // 0x20: 进程创建状态码,驱动程序可修改此值来阻止进程创建
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

PCREATE_PROCESS_NOTIFY_ROUTINE_EX 回调函数是通过 PsSetCreateProcessNotifyRoutineEx 函数注册或删除的,注意不要与 PsSetCreateProcessNotifyRoutine 混用。

1
2
3
4
5
6
NTKERNELAPI 
NTSTATUS
PsSetCreateProcessNotifyRoutineEx (
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, // 扩展回调函数,提供更多的进程上下文信息
_In_ BOOLEAN Remove // 布尔值,TRUE 表示移除回调,FALSE 表示注册回调
);

提示

PsSetCreateProcessNotifyRoutineEx 可能因未签名驱动被拒绝注册回调,返回错误码:STATUS_ACCESS_DENIED (0xC0000022)

通过分析发现是 MmVerifyCallbackFunction 函数未通过回调函数的校验返回 FALSE 导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTKERNELAPI
NTSTATUS
PsSetCreateProcessNotifyRoutineEx (
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
_In_ BOOLEAN Remove
)
{
return PspSetCreateProcessNotifyRoutine(NotifyRoutine, Remove, TRUE);
}

NTKERNELAPI
NTSTATUS
PspSetCreateProcessNotifyRoutine (
PVOID NotifyRoutine,
BOOLEAN Remove,
BOOLEAN IsExtended
)
{
// [...]
if (IsExtended && !MmVerifyCallbackFunction(NotifyRoutine)) {
return STATUS_ACCESS_DENIED; // 👈 返回 0xC0000022 错误码
}
// [...]
}

MmVerifyCallbackFunction 函数会调用 MiLookupDataTableEntry 获取回调函数 NotifyRoutine 对应的 PKLDR_DATA_TABLE_ENTRY 结构,然后根据其中的 Flags 字段是否设置了 0x20 标志位确定是否返回 TRUE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTKERNELAPI
BOOLEAN
MmVerifyCallbackFunction (
PVOID NotifyRoutine
)
{
// [...]
DataTableEntry = MiLookupDataTableEntry(NotifyRoutine, TRUE);
if (DataTableEntry) {
IsValidCallback = (DataTableEntry->Flags & 0x20) != 0;
}
// [...]
return IsValidCallback;
}

MiLookupDataTableEntry 会遍历所有已加载模块,判断传入的回调函数在哪个模块的地址范围内,返回该模块对应的 KLDR_DATA_TABLE_ENTRY 结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
PKLDR_DATA_TABLE_ENTRY
MiLookupDataTableEntry (
IN PVOID AddressWithinSection, // 输入参数:要在模块中查找的内存地址
IN ULONG ResourceHeld // 输入参数:指示是否已持有PsLoadedModuleResource资源锁
)
/*++

函数说明:

本函数查找包含指定地址的已加载模块对应的数据表条目。

参数说明:

AddressWithinSection - 提供要查找的地址,该地址应包含在目标模块的代码段或数据段中

ResourceHeld - 指示调用者是否已持有已加载模块资源锁:
TRUE = 调用者已持有锁,不需要重复获取
FALSE = 调用者未持有锁,函数内部需要获取锁

返回值:

返回映射到参数地址的已加载模块列表数据表条目的地址,如果未找到则返回NULL

--*/
{
PKTHREAD CurrentThread; // 当前线程指针
PKLDR_DATA_TABLE_ENTRY DataTableEntry; // 遍历中的当前数据表条目
PKLDR_DATA_TABLE_ENTRY FoundEntry; // 找到的目标条目
PLIST_ENTRY NextEntry; // 指向加载模块列表中的下一个条目

PAGED_CODE(); // 声明此函数可分页,但仅在安全环境下调用

FoundEntry = NULL; // 初始化查找结果为未找到

//
// 遍历已加载模块列表,查找包含指定地址的模块
// 注意:如果模块正在卸载过程中,可能暂时不在列表中
//

// 如果调用者未持有资源锁,则在此处获取锁
if (!ResourceHeld) {
CurrentThread = KeGetCurrentThread();
// 进入关键区域并禁用APC
KeEnterCriticalRegionThread(CurrentThread);
// 以共享模式获取已加载模块资源锁
ExAcquireResourceSharedLite(&PsLoadedModuleResource, TRUE);
}
else {
// 调用者已持有锁,不需要额外获取
CurrentThread = NULL;
}

// 从已加载模块列表头部开始遍历
NextEntry = PsLoadedModuleList.Flink;
ASSERT(NextEntry != NULL); // 确保列表已初始化

// 遍历整个模块链表
do {
// 通过链表指针获取当前模块的数据表条目
DataTableEntry = CONTAINING_RECORD(NextEntry,
KLDR_DATA_TABLE_ENTRY,
InLoadOrderLinks);

//
// 📌检查当前模块的内存范围是否包含目标地址
//
if (AddressWithinSection >= DataTableEntry->DllBase && // 地址 >= 模块基址
AddressWithinSection < (PVOID)((PUCHAR)DataTableEntry->DllBase +
DataTableEntry->SizeOfImage)) // 地址 < 模块结束地址
{
FoundEntry = DataTableEntry; // 找到匹配模块
break; // 跳出循环
}

// 移动到下一个模块
NextEntry = NextEntry->Flink;
} while (NextEntry != &PsLoadedModuleList); // 直到回到列表头部

// 如果此函数获取了锁,需要释放它
if (CurrentThread != NULL) {
ExReleaseResourceLite(&PsLoadedModuleResource); // 释放资源锁
KeLeaveCriticalRegionThread(CurrentThread); // 退出关键区域
}

return FoundEntry; // 返回查找到的模块条目或NULL
}

实际上上述检查只是判断驱动是否经过微软官方签名。而绕过方法也很简单,我们只需要修改驱动对应的 KLDR_DATA_TABLE_ENTRY 结构的 Flags 字段,使其 0x20 置位即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct _KLDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
ULONG __Undefined1;
ULONG __Undefined2;
ULONG __Undefined3;
ULONG NonPagedDebugInfo;
ULONG DllBase;
ULONG EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
// ...(后续字段省略)
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
PKLDR_DATA_TABLE_ENTRY DataTableEntry;

// 绕过驱动签名检测
DataTableEntry = (PKLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
DataTableEntry->Flags |= 0x20;

// 后续 PsSetCreateProcessNotifyRoutineEx 注册进程回调函数
// [...]

DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

进程回调分析

回调注册

无论是否是扩展回调,进程回调最终都是通过 PspSetCreateProcessNotifyRoutine 函数注册的,而这个函数的第三个参数用来表示是否是扩展回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NTKERNELAPI
NTSTATUS
PsSetCreateProcessNotifyRoutineEx (
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
_In_ BOOLEAN Remove
)
{
return PspSetCreateProcessNotifyRoutine(NotifyRoutine, Remove, TRUE);
}

NTKERNELAPI
NTSTATUS
PsSetCreateProcessNotifyRoutine (
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
_In_ BOOLEAN Remove
)
{
return PspSetCreateProcessNotifyRoutine(NotifyRoutine, Remove, TRUE);
}

NTKERNELAPI
NTSTATUS
PspSetCreateProcessNotifyRoutine (
PVOID NotifyRoutine,
BOOLEAN Remove,
BOOLEAN IsExtended
);

PspSetCreateProcessNotifyRoutine 函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
NTSTATUS
PspSetCreateProcessNotifyRoutine(
_In_ PVOID NotifyRoutine,
_In_ BOOLEAN Remove,
_In_ BOOLEAN IsExtended
)
{
PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock;
PPSP_NOTIFY_ENTRY NotifyEntry;
ULONG i;

//
// 注销路径:移除已注册的回调
//
if (Remove)
{
PKTHREAD CurrentThread = KeGetCurrentThread();
KeEnterCriticalRegionThread(CurrentThread);

for (i = 0; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++)
{
NotifyEntry->CallbackBlock = &PspCreateProcessNotifyRoutine[i];
CallbackBlock = ExReferenceCallBackBlock(NotifyEntry);

if (CallbackBlock == NULL)
continue;

//
// 检查回调函数是否匹配
//
if ((PVOID)ExGetCallBackBlockRoutine(CallbackBlock) == NotifyRoutine)
{
//
// ⚠️类型必须匹配:不能用 EX 注销普通回调,反之亦然
//
if ((IsExtended && !NotifyEntry->IsExtended) ||
(!IsExtended && NotifyEntry->IsExtended))
{
ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock);
continue;
}

//
// 原子清除该回调槽
//
if (ExCompareExchangeCallBack(
(PEX_CALLBACK)&NotifyEntry->CallbackBlock,
NULL,
CallbackBlock))
{
//
// 更新回调计数
//
if (IsExtended)
InterlockedDecrement(&PspCreateProcessNotifyRoutineExCount);
else
InterlockedDecrement(&PspCreateProcessNotifyRoutineCount);

ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock);

KeLeaveCriticalRegionThread(CurrentThread);

//
// 等待所有正在执行的回调完成并释放资源
//
ExWaitForCallBacks(CallbackBlock);
ExFreeCallBack(CallbackBlock);

return STATUS_SUCCESS;
}
}

//
// 匹配失败,释放引用
//
ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock);
}

KeLeaveCriticalRegionThread(CurrentThread);
return STATUS_PROCEDURE_NOT_FOUND;
}

//
// ⚠️EX 回调需验证是否为有效可回调地址(一般要求驱动签名)
//
if (IsExtended && !MmVerifyCallbackFunction(NotifyRoutine))
return STATUS_ACCESS_DENIED;

//
// 分配新的回调块
//
CallbackBlock = ExAllocateCallBack((PEX_CALLBACK_FUNCTION)NotifyRoutine, NULL);
if (!CallbackBlock)
return STATUS_INSUFFICIENT_RESOURCES;

//
// 插入到空闲槽
//
for (i = 0; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++)
{
NotifyEntry->CallbackBlock = &PspCreateProcessNotifyRoutine[i];

if (ExCompareExchangeCallBack(
NotifyEntry->CallbackBlock,
CallbackBlock,
NULL))
{
//
// 设置 EX 标志
//
NotifyEntry->IsExtended = IsExtended;

if (IsExtended)
{
InterlockedIncrement(&PspCreateProcessNotifyRoutineExCount);

if (!(PspNotifyEnableMask & PSP_NOTIFY_PROCESS_EX))
{
InterlockedBitTestAndSet(&PspNotifyEnableMask, PSP_NOTIFY_PROCESS_EX_BIT); // 设置 bit2
}
}
else
{
InterlockedIncrement(&PspCreateProcessNotifyRoutineCount);

if (!(PspNotifyEnableMask & PSP_NOTIFY_PROCESS))
{
InterlockedBitTestAndSet(&PspNotifyEnableMask, PSP_NOTIFY_PROCESS_BIT); // 设置 bit1
}
}

return STATUS_SUCCESS;
}
}

//
// 没有空位,释放回调块
//
ExFreeCallBack(CallbackBlock);
return STATUS_INVALID_PARAMETER;
}

对于注销流程,该函数的处理逻辑如下:

  1. 进入线程临界区,防止回调数组竞争;
  2. 遍历回调槽 PspCreateProcessNotifyRoutine[i]
    • 如果 NotifyRoutine 匹配,并且回调类型也匹配(EX/普通):
      • 使用 ExCompareExchangeCallBack() 原子替换为 NULL
      • 更新计数(PspCreateProcessNotifyRoutineCountExCount);
      • 等待所有正在执行的回调返回;
      • 调用 ExFreeCallBack() 释放回调资源;
      • 返回 STATUS_SUCCESS
    • 否则,释放引用,继续查找;
  3. 如果未找到匹配回调,返回 STATUS_PROCEDURE_NOT_FOUND

对于注册流程,该函数的处理逻辑如下:

  1. 调用 MmVerifyCallbackFunction 校验函数地址安全性(如签名);
    • 失败则返回 STATUS_ACCESS_DENIED
  2. 使用 ExAllocateCallBack() 创建 CallbackBlock
    • 失败返回 STATUS_INSUFFICIENT_RESOURCES
  3. 遍历回调槽,尝试原子插入:
    • 插入成功则:
      • 设置 IsExtended 标志;
      • 更新对应计数;
      • 设置 PspNotifyEnableMask 中对应 bit(普通/EX);
      • 返回 STATUS_SUCCESS
  4. 所有槽都被占用时,释放分配的回调,返回 STATUS_INVALID_PARAMETER

提示

  • 回调槽 PspCreateProcessNotifyRoutine 大小为 PSP_MAX_CREATE_PROCESS_NOTIFY(64),也就是说我们最多注册 64 个进程回调函数(包括扩展回调和非扩展回调)。
  • 回调函数会按照先后循序放入回调槽,越早注册的函数越靠前,后续也会越早调用,方便拦截后续的回调函数。

回调调用

对于进程创建事件,进程回调函数会在 PspInsertThread 函数中被调用,这是因为第一个线程创建的时候一定是进程创建的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
if (Process->ActiveThreads == 1)  // 创建主线程后首次触发进程通知
{
EtwTraceProcess(Process, ETW_EVENT_PROCESS_CREATE); // ETW 跟踪:创建进程事件(例如 PID 映射)

NTSTATUS CreationStatus = STATUS_SUCCESS;
BOOLEAN HasExNotify = (PspNotifyEnableMask & PSP_NOTIFY_PROCESS_EX) != 0; // 是否启用扩展进程通知(Ex)

// 是否启用了任何进程通知机制(普通或扩展)
if (PspNotifyEnableMask & (PSP_NOTIFY_PROCESS | PSP_NOTIFY_PROCESS_EX))
{
PFILE_OBJECT FileObject = NULL;
BOOLEAN NeedDereference = FALSE;
PS_CREATE_NOTIFY_INFO NotifyInfo;

// -------------------------------
// 构建扩展通知结构(EX 回调使用)
// -------------------------------
if (HasExNotify)
{
NotifyInfo.Size = sizeof(NotifyInfo);
NotifyInfo.Flags = 0;
NotifyInfo.ParentProcessId = Process->InheritedFromUniqueProcessId;

// 获取当前线程 CID(Creator)
NotifyInfo.CreatingThreadId = CONTAINING_RECORD(
KeGetCurrentThread(), _ETHREAD, Tcb)->Cid;

NotifyInfo.CreationStatus = STATUS_SUCCESS;

// 获取进程对应的文件对象(镜像句柄)
if (CreateProcessContext && CreateProcessContext->FileObject)
{
FileObject = CreateProcessContext->FileObject;
}
else
{
NeedDereference = TRUE;
PsReferenceProcessFilePointer(Process, &FileObject);
}

NotifyInfo.FileObject = FileObject;

// 判断是否提供了映像文件名
if (CreateProcessContext && (CreateProcessContext->Flags & 0x20))
{
NotifyInfo.ImageFileName = &CreateProcessContext->ImageFileName;
NotifyInfo.FileOpenNameAvailable = TRUE;
}
else
{
NotifyInfo.ImageFileName = &FileObject->FileName;
}

// 获取命令行参数(以 UNICODE_STRING 形式)
if (CreateProcessContext && CreateProcessContext->CommandLine)
{
// 通常为 RTL_USER_PROCESS_PARAMETERS 的偏移
NotifyInfo.CommandLine = CreateProcessContext->CommandLine + sizeof(UNICODE_STRING);
}
else
{
NotifyInfo.CommandLine = NULL;
}
}

// -------------------------------
// 遍历注册的进程回调函数表
// -------------------------------
EX_CALLBACK_ROUTINE_BLOCK* CallbackBlock;
for (int i = 0; i < MAX_CREATE_PROCESS_NOTIFY; ++i)
{
CallbackBlock = ExReferenceCallBackBlock(&PspCreateProcessNotifyRoutine[i]);
if (!CallbackBlock)
continue;

// 获取注册的回调函数地址
PVOID NotifyRoutine = ExGetCallBackBlockRoutine(CallbackBlock);

if (CallbackBlock->Context)
{
// 扩展回调(带结构体)
if (HasExNotify)
{
((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)NotifyRoutine)(
Process,
Process->UniqueProcessId,
&NotifyInfo);
}
}
else
{
// 普通回调(旧式三参数)
((PCREATE_PROCESS_NOTIFY_ROUTINE)NotifyRoutine)(
Process->InheritedFromUniqueProcessId,
Process->UniqueProcessId,
TRUE); // TRUE 表示创建
}

ExDereferenceCallBackBlock(CallbackBlock);

// -------------------------------
// 回调请求中止进程创建
// -------------------------------
if (HasExNotify && !NT_SUCCESS(NotifyInfo.CreationStatus))
{
PsTerminateProcess(Process, NotifyInfo.CreationStatus);
break;
}
}

// 如果我们手动引用了文件对象,需要释放
if (NeedDereference)
ObfDereferenceObject(FileObject);
}
}

提示

  • PspNotifyEnableMask 是一个全局的位掩码变量,用于控制系统通知类回调(如进程、线程、镜像加载等)的启用状态。它的每一位分别表示某一类回调是否启用。

    对于进程回调:

    • bit 1:即代码中的 PSP_NOTIFY_PROCESS_BIT,用于控制普通的 PsSetCreateProcessNotifyRoutine 回调是否启用。
    • bit 2:即代码中的 PSP_NOTIFY_PROCESS_EX_BIT,用于控制扩展的 PsSetCreateProcessNotifyRoutineEx 回调是否启用。

    因此我们可以短暂清空相关标志位来规避一些敏感行为的监控。

  • 如果是扩展回调,则设置扩展回调的结构体中 PS_CREATE_NOTIFY_INFO 中的 CreationStatus 可以阻止进程创建。

  • NotifyInfo 在调用扩展回调函数的过程中是共用的,因此我们可以通过修改 NotifyInfo 结构体中的内容从而影响到后续扩展回调函数的判断;同理,Process 结构体也可以被修改。

对于进程退出事件则是在 PspExitProcess 函数中调用的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//
// 如果启用了进程回调通知(普通或扩展)
//
if (PspNotifyEnableMask & (PSP_NOTIFY_PROCESS | PSP_NOTIFY_PROCESS_EX))
{
ULONG i;
PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock;
PCREATE_PROCESS_NOTIFY_ROUTINE CallbackFunction;
PPSP_NOTIFY_ENTRY NotifyEntry = PspCreateProcessNotifyRoutine;
BOOLEAN ExtendedEnabled = (PspNotifyEnableMask & PSP_NOTIFY_PROCESS_EX) != 0;

for (i = 0; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++, NotifyEntry++)
{
//
// 尝试引用该回调槽的回调块(如果存在)
//
CallbackBlock = ExReferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock);
if (CallbackBlock == NULL)
continue;

//
// 如果类型不匹配则跳过(普通回调不调用扩展,反之亦然)
//
if (NotifyEntry->IsExtended && !ExtendedEnabled)
{
ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock);
continue;
}

//
// 提取回调函数指针并调用(Create=FALSE 表示进程退出)
//
CallbackFunction = (PCREATE_PROCESS_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(CallbackBlock);
CallbackFunction(
NotifyEntry->IsExtended ? Process->InheritedFromUniqueProcessId : Process->UniqueProcessId,
Process->UniqueProcessId,
FALSE
);

//
// 释放引用
//
ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock);
}
}

线程回调

线程回调是 Windows 内核提供的重要机制之一,允许驱动在线程创建与销毁事件发生时接收通知,以实现监控、日志记录、安全检测等功能。Windows 提供了线程回调函数 PCREATE_THREAD_NOTIFY_ROUTINE

1
2
3
4
5
typedef VOID (*PCREATE_THREAD_NOTIFY_ROUTINE)(
_In_ HANDLE ProcessId, // 所属进程的 PID
_In_ HANDLE ThreadId, // 线程 ID
_In_ BOOLEAN Create // TRUE 表示线程创建,FALSE 表示线程销毁
);

基础回调注册

与进程回调不同,线程回调函数 PCREATE_THREAD_NOTIFY_ROUTINE 的注册和移除函数是两个不同的函数:

  • 注册线程回调

    1
    2
    3
    4
    5
    NTKERNELAPI
    NTSTATUS
    PsSetCreateThreadNotifyRoutine(
    _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
    );
  • 移除线程回调

    1
    2
    3
    4
    5
    NTKERNELAPI
    NTSTATUS
    PsRemoveCreateThreadNotifyRoutine(
    _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
    );

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
VOID MyThreadNotifyCallback(
HANDLE ProcessId,
HANDLE ThreadId,
BOOLEAN Create
)
{
if (Create) {
DbgPrint("线程创建: PID=%p, TID=%p\n", ProcessId, ThreadId);
} else {
DbgPrint("线程销毁: PID=%p, TID=%p\n", ProcessId, ThreadId);
}
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
PsSetCreateThreadNotifyRoutine(MyThreadNotifyCallback);
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
PsRemoveCreateThreadNotifyRoutine(MyThreadNotifyCallback);
}

扩展回调注册

PsSetCreateThreadNotifyRoutineEx 是 Windows 10 引入的增强线程回调接口,允许驱动程序注册用于监听系统范围内线程创建和销毁事件的回调函数。

该函数比旧的 PsSetCreateThreadNotifyRoutine 更灵活,支持控制回调类型(如是否在被创建线程的上下文中调用),并允许特权驱动在系统范围内对线程生命周期进行精细监控。

PsSetCreateThreadNotifyRoutineEx 函数声明如下:

1
2
3
4
5
6
NTKERNELAPI
NTSTATUS
PsSetCreateThreadNotifyRoutineEx(
_In_ PSCREATETHREADNOTIFYTYPE NotifyType, // 枚举类型,表示要注册哪种类型的线程通知机制
_In_ PVOID NotifyInformation // 实际上是 PCREATE_THREAD_NOTIFY_ROUTINE 的函数指针
);

提示

PsSetCreateThreadNotifyRoutineEx 注册的回调函数依旧使用 PsRemoveCreateThreadNotifyRoutine 卸载。

其中 PSCREATETHREADNOTIFYTYPE 目前支持下面两种值:

1
2
3
4
typedef enum _PSCREATETHREADNOTIFYTYPE {
PsCreateThreadNotifyNonSystem = 0, // 回调在新线程上执行
PsCreateThreadNotifySubsystems = 1 // 回调为所有子系统线程调用
} PSCREATETHREADNOTIFYTYPE;

线程回调分析

回调注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//
// 注册线程创建/退出通知回调(普通回调)
//
NTSTATUS
PsSetCreateThreadNotifyRoutine(
_In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
)
{
PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock;
PEX_CALLBACK CallbackSlot;
ULONG Index;

PAGED_CODE();

//
// 分配一个回调块,用于封装回调函数。
//
CallbackBlock = ExAllocateCallBack((PEX_CALLBACK_FUNCTION)NotifyRoutine, NULL);
if (CallbackBlock == NULL)
return STATUS_INSUFFICIENT_RESOURCES;

//
// 在回调表中查找空槽插入回调。
//
for (Index = 0; Index < PSP_MAX_CREATE_THREAD_NOTIFY; Index++)
{
CallbackSlot = &PspCreateThreadNotifyRoutine[Index];

if (ExCompareExchangeCallBack(
CallbackSlot,
CallbackBlock,
NULL))
{
//
// 成功注册,更新回调计数。
//
InterlockedIncrement(&PspCreateThreadNotifyRoutineCount);

//
// 如果尚未启用线程通知,设置对应的启用掩码位。
//
if (!(PspNotifyEnableMask & PSP_NOTIFY_THREAD))
{
InterlockedBitTestAndSet(&PspNotifyEnableMask, PSP_NOTIFY_THREAD_BIT); // 设置 bit3
}

return STATUS_SUCCESS;
}
}

//
// 未找到空槽,释放分配的回调块。
//
ExFreeCallBack(CallbackBlock);
return STATUS_INSUFFICIENT_RESOURCES;
}

回调移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
NTSTATUS
PsRemoveCreateThreadNotifyRoutine(
_In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
)
{
PKTHREAD CurrentThread;
PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock;
ULONG i;

PAGED_CODE();

//
// 禁用 APC 交付(进入关键区域)
//
CurrentThread = KeGetCurrentThread();
KeEnterCriticalRegionThread(CurrentThread);

for (i = 0; i < PSP_MAX_CREATE_THREAD_NOTIFY; i++)
{
//
// 获取回调块引用,以便检查其实际回调函数
//
CallbackBlock = ExReferenceCallBackBlock(&PspCreateThreadNotifyRoutine[i]);
if (CallbackBlock == NULL)
continue;

//
// 比较是否为目标回调函数
//
if ((PCREATE_THREAD_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(CallbackBlock) == NotifyRoutine)
{
//
// 原子移除该回调块
//
if (ExCompareExchangeCallBack(
&PspCreateThreadNotifyRoutine[i],
NULL,
CallbackBlock))
{
//
// 减少注册计数
//
InterlockedDecrement(&PspCreateThreadNotifyRoutineCount);

ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[i], CallbackBlock);

KeLeaveCriticalRegionThread(CurrentThread);

//
// 等待所有执行中的回调完成并释放资源
//
ExWaitForCallBacks(CallbackBlock);
ExFreeCallBack(CallbackBlock);

return STATUS_SUCCESS;
}
}

//
// 非匹配或交换失败,释放引用
//
ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[i], CallbackBlock);
}

//
// 所有回调槽都未找到匹配项
//
KeLeaveCriticalRegionThread(CurrentThread);
return STATUS_PROCEDURE_NOT_FOUND;
}

回调调用

对于线程创建事件,线程回调函数在 PspInsertThread 函数中被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
// 线程创建通知(无论是不是主线程)
//
if (PspCreateThreadNotifyRoutineCount != 0) {
ULONG Index;
PEX_CALLBACK_ROUTINE_BLOCK Callback;
PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine;

for (Index = 0; Index < PSP_MAX_CREATE_THREAD_NOTIFY; Index++) {
Callback = ExReferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index]);
if (Callback != NULL) {
NotifyRoutine = (PCREATE_THREAD_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(Callback);

NotifyRoutine(CONTAINING_RECORD(Object->Tcb.Process, _EPROCESS, Pcb)->UniqueProcessId,
Thread->Cid.UniqueThread,
TRUE);

ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index], Callback);
}
}
}

对于线程退出事件,线程回调函数在 PspExitThread 函数中被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//
// 如果启用了线程回调(对应 bit 3),则遍历并调用回调函数
//
if (PspNotifyEnableMask & PSP_NOTIFY_THREAD)
{
PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock;
PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine;
ULONG Index;

for (Index = 0; Index < PSP_MAX_CREATE_THREAD_NOTIFY; Index++)
{
//
// 尝试引用回调块,防止并发释放
//
CallbackBlock = ExReferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index]);

if (CallbackBlock)
{
//
// 获取回调函数指针
//
NotifyRoutine = (PCREATE_THREAD_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(CallbackBlock);

//
// 执行回调:通知线程即将退出(第三个参数为 FALSE)
//
NotifyRoutine(
CONTAINING_RECORD(CurThread->Tcb.Process, _EPROCESS, Pcb)->UniqueProcessId, // 进程 ID
CurThread->Cid.UniqueThread, // 线程 ID
FALSE // FALSE 表示线程退出
);

//
// 解除回调块的引用
//
ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index], CallbackBlock);
}
}
}

模块回调

模块回调是 Windows 内核提供的通知机制之一,允许驱动程序在任何模块(包括 EXE、DLL、SYS)加载到进程地址空间时收到通知,以便实现监控、日志记录、DLL 注入检测、签名验证等功能。

Windows 提供的模块回调函数类型为:

1
2
3
4
5
typedef VOID (*PLOAD_IMAGE_NOTIFY_ROUTINE)(
_In_opt_ PUNICODE_STRING FullImageName, // 映像完整路径(可能为 NULL)
_In_ HANDLE ProcessId, // 映像加载目标进程 ID(0 表示系统进程)
_In_ PIMAGE_INFO ImageInfo // 映像加载信息结构体指针
);

其中 PIMAGE_INFO 结构体描述了加载的映像的地址、大小和加载类型等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _IMAGE_INFO {
union {
ULONG Properties; // 0x00: 属性标志位集合(用于快速判断加载类型)

struct {
ULONG ImageAddressingMode : 8; // [bit 0–7] 映像寻址模式:0 = 32位,1 = 64位,其他值为保留
ULONG SystemModeImage : 1; // [bit 8] 📌映像是否在内核模式下加载(例如内核驱动)
ULONG ImageMappedToAllPids : 1; // [bit 9] 是否映射到所有进程(例如共享模块)
ULONG ExtendedInfoPresent : 1; // [bit 10] 是否存在扩展映像信息(IMAGE_INFO_EX 结构)
ULONG MachineTypeMismatch : 1; // [bit 11] 架构不匹配标志(如加载 x86 模块到 x64)
ULONG ImageSignatureLevel : 4; // [bit 12–15]映像签名级别(Windows Defender 驱动执行控制级别)
ULONG ImageSignatureType : 3; // [bit 16–18]映像签名类型(PE 签名、Catalog 等)
ULONG ImagePartialMap : 1; // [bit 19] 如果整个映像未完全映射则为 1(通常用于特殊加载方式)
ULONG Reserved : 12; // [bit 20–31]保留字段(必须为 0,供系统未来使用)
};
};

PVOID ImageBase; // 0x04 / 0x08: 映像映射到进程地址空间中的基地址
ULONG ImageSelector; // 0x08 / 0x10: 保留字段,仅用于 x86(段选择器,现代系统基本无用)
SIZE_T ImageSize; // 0x0C / 0x18: 映像的大小(以字节为单位)
ULONG ImageSectionNumber; // 0x10 / 0x20: 所加载映像在 PE 文件中的节索引(可用于调试或定位符号)

} IMAGE_INFO, *PIMAGE_INFO;

基础回调注册

模块回调函数 PLOAD_IMAGE_NOTIFY_ROUTINE 的注册和移除函数是两个不同的函数:

  • 注册模块回调

    1
    2
    3
    4
    5
    NTKERNELAPI
    NTSTATUS
    PsSetLoadImageNotifyRoutine(
    _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
    );
  • 移除模块回调

    1
    2
    3
    4
    5
    NTKERNELAPI
    NTSTATUS
    PsRemoveLoadImageNotifyRoutine(
    _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
    );

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
VOID ImageLoadCallback(
PUNICODE_STRING FullImageName,
HANDLE ProcessId,
PIMAGE_INFO ImageInfo
)
{
if (FullImageName && ProcessId != 0) {
DbgPrint("进程 %p 加载映像:%wZ,基址=%p,大小=0x%Ix\n",
ProcessId,
FullImageName,
ImageInfo->ImageBase,
ImageInfo->ImageSize);
}
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
PsSetLoadImageNotifyRoutine(ImageLoadCallback);
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
PsRemoveLoadImageNotifyRoutine(ImageLoadCallback);
}

提示

模块回调的开关位于 PspNotifyEnableMask 的最低位。

1
#define PSP_NOTIFY_IMAGE 1

扩展回调注册

PsSetLoadImageNotifyRoutineEx 是 Win10 引入的模块回调注册函数,相比于旧的 PsSetLoadImageNotifyRoutine,此版本支持传入 Flags 参数,以便控制回调行为,如是否通知跨架构(x86/x64)映像等。

1
2
3
4
NTSTATUS PsSetLoadImageNotifyRoutineEx(
_In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine,
_In_ ULONG_PTR Flags
);

其中 Flags 目前官方仅定义支持 PS_IMAGE_NOTIFY_CONFLICTING_ARCHITECTURE(0x1),表示启用跨架构通知(例如:在 x64 系统上也通知加载 x86 模块)。

对象回调(Object Callbacks)

早期防护软件为了拦截对进程、线程等敏感对象的句柄创建/复制,普遍使用 SSDT/内核钩子等侵入式技术,极易引发系统不稳定。自 Windows Vista SP1 / Windows Server 2008 起,微软在 Object Manager 上提供了 对象回调机制(Object Callbacks)

在Windows内核中,对象回调机制(Object Callbacks)是一种安全和监控机制,允许内核模式驱动程序注册特定的回调函数,以监视或拦截对某些内核对象(例如进程、线程、文件等)的操作。

对象回调使用

回调注册卸载

对象回调机制提供了专门的内核函数,用于驱动程序注册和卸载回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//
// 注册对象访问回调(例如进程、线程、注册表键等的句柄创建/复制操作)
// 允许驱动拦截并修改访问权限,或进行安全检查与日志记录
//
NTKERNELAPI
NTSTATUS
ObRegisterCallbacks (
_In_ POB_CALLBACK_REGISTRATION CallbackRegistration, // 指向 OB_CALLBACK_REGISTRATION 结构体:
// - 指定监控的对象类型(如 PsProcessType)
// - 指定回调函数(PreOperation / PostOperation)
// - 指定监控的操作类型(创建句柄 / 复制句柄)
// - Altitude:回调优先级字符串(必须唯一)
_Outptr_ PVOID *RegistrationHandle // [输出] 返回注册后的句柄(保存驱动内部变量中),
// 可用于后续通过 ObUnRegisterCallbacks 取消回调
);

//
// 注销回调函数,释放先前注册的监控行为
// 若驱动卸载前未注销,将导致 BSOD(挂掉)
//
NTKERNELAPI
VOID
ObUnRegisterCallbacks (
_In_ PVOID RegistrationHandle // 通过 ObRegisterCallbacks 返回的注册句柄
);

提示

ObRegisterCallbacks 同样要求驱动签名,因此依旧需要进程扩展回调的绕过方法。

其中 OB_CALLBACK_REGISTRATION 结构用于描述一次对象访问回调注册请求,包含回调的元信息、目标对象类型、回调函数数组等。该结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
///////////////////////////////////////////////////////////////////////////////
// 结构体: OB_CALLBACK_REGISTRATION
// 用途: 用于描述一次对象访问回调注册请求,包含回调的元信息、目标对象类型、回调函数数组等。
//
// 常用于调用 ObRegisterCallbacks 时填写,注册用于监控进程、线程、注册表等对象访问行为。
///////////////////////////////////////////////////////////////////////////////
typedef struct _OB_CALLBACK_REGISTRATION {
//
// 结构体版本号,必须设置为 OB_FLT_REGISTRATION_VERSION(当前为 0x100)
//
USHORT Version;

//
// OperationRegistration 数组的元素数量,即要注册的对象类型数量(通常为 1~3)
//
USHORT OperationRegistrationCount;

//
// 回调高度字符串,用于排序执行顺序。
// Altitude 必须是全系统唯一的字符串,否则注册失败(建议格式:<数字>.<驱动/产品标识符>)
// 越低的值越早执行(安全软件常注册较低值以优先拦截)
//
UNICODE_STRING Altitude;

//
// 自定义上下文指针,在 PreOperation/PostOperation 回调中可通过 RegistrationContext 参数获取
// 可用于传递驱动自己的状态结构等(可选)
//
PVOID RegistrationContext;

//
// 指向 OB_OPERATION_REGISTRATION 数组,每个元素表示一个对象类型及其操作回调配置
// 通常设置一个或多个项,例如进程对象、线程对象等
//
OB_OPERATION_REGISTRATION* OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;

提示

  • Version: 结构体版本号,必须设置为 OB_FLT_REGISTRATION_VERSION(0x100),但是为了防止未来的兼容性问题,建议使用 ObGetFilterVersion 函数动态获取
  • Altitude:微软在内核回调机制(尤其是对象管理器回调文件过滤驱动)中用于排序执行优先级的机制,本质上是一个全局唯一的字符串表示的“高度值”。为了同时支持优先级排序阅读与区分,要求格式为 <数字>.<驱动/产品标识符>,例如 320000.MyDriverName
  • RegistrationContext:定义上下文指针,会作为参数 RegistrationContext 传递给每次触发的 PreOperation / PostOperation 回调函数。

我们注册的回调函数位于 OperationRegistration 指向的 OB_OPERATION_REGISTRATION 结构体数组,其中 OperationRegistrationCount 表示数组中元素的数量。OB_OPERATION_REGISTRATION 结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
///////////////////////////////////////////////////////////////////////////////
// 结构体: OB_OPERATION_REGISTRATION
// 用途: 描述驱动希望拦截的对象类型、操作类型以及对应的回调函数。
// 每个 OB_OPERATION_REGISTRATION 项表示一类对象(如进程、线程)的访问行为拦截配置。
//
// 此结构体是 OB_CALLBACK_REGISTRATION 中的 OperationRegistration 成员的元素类型。
///////////////////////////////////////////////////////////////////////////////
typedef struct _OB_OPERATION_REGISTRATION {
//
// 指向要监控的对象类型的全局指针,例如:
// - PsProcessType 表示进程对象("Process")
// - PsThreadType 表示线程对象("Thread")
// - ExEventObjectType 表示事件对象
// - CmKeyObjectType 表示注册表键(需包含 ntddk.h 并开启特定版本支持)
//
// 注:这些对象类型由系统导出,不能自己构造;也不能比较字符串名称。
//
_In_ POBJECT_TYPE *ObjectType;

//
// 要拦截的操作类型,可以是以下之一或组合:
// - OB_OPERATION_HANDLE_CREATE:表示用户创建对象句柄(如 ZwOpenProcess)
// - OB_OPERATION_HANDLE_DUPLICATE:表示复制已有对象句柄(如 DuplicateHandle)
//
// 示例:拦截创建和复制操作
// Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
//
_In_ OB_OPERATION Operations;

//
// 预操作回调函数,在对象句柄创建或复制之前被调用。
// 可以检查访问权限、对象属性等,甚至拒绝访问。
//
// 回调函数类型为:
// OB_PREOP_CALLBACK_STATUS
// (*POB_PRE_OPERATION_CALLBACK)(_Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation);
//
// 返回 OB_PREOP_SUCCESS 表示继续;
// 返回 OB_PREOP_COMPLETE 表示取消,并跳过后续操作。
//
_In_ POB_PRE_OPERATION_CALLBACK PreOperation;

//
// 后操作回调函数,在句柄创建/复制完成后调用(可选)
// 一般用于记录日志或执行收尾操作,不能修改访问权限。
//
// 回调函数类型为:
// VOID (*POB_POST_OPERATION_CALLBACK)(_In_ POB_POST_OPERATION_INFORMATION OperationInformation);
//
// 若不需要后处理,可设为 NULL。
//
_In_ POB_POST_OPERATION_CALLBACK PostOperation;

} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

这个结构体描述了我们要注册的回调函数,以及回调函数监控的对象事件

  • 首先监控对象通过 ObjectType 字段描述,这个字段描述要拦截的对象种类,如进程、线程、注册表项等。我们需要将其填充为一个系统导出的全局的对象类型描述符指针。

    对象名称 符号 说明
    进程 PsProcessType 所有 EPROCESS 对象
    线程 PsThreadType 所有 ETHREAD 对象
    注册表键 CmKeyObjectType 所有注册表项对象(KEY_OBJECT
    事件 ExEventObjectType 事件对象(非用户事件)
    信号量等 ExSemaphoreObjectType 同步对象(部分可见)
  • 监控事件通过 Operations 字段描述,用于指定拦截的操作类型(是句柄的创建,还是句柄的复制)。该字段是一个位标志(flags),值可以为以下之一或组合:

    1
    2
    #define OB_OPERATION_HANDLE_CREATE     0x00000001  // 例如 ZwOpenProcess
    #define OB_OPERATION_HANDLE_DUPLICATE 0x00000002 // 例如 DuplicateHandle
    • OB_OPERATION_HANDLE_CREATE:当系统执行创建一个新对象句柄(handle)时触发此事件,也就是从用户态请求访问某个对象,比如进程、线程、注册表项等,进入内核后准备返回一个句柄。
    • OB_OPERATION_HANDLE_DUPLICATE:当系统执行复制一个已存在的对象句柄时触发此事件,也就是一个进程希望将已有句柄复制到另一个进程的句柄表中。对应的 API 为 ZwDuplicateObject
  • OB_OPERATION_REGISTRATION 中提供两种类型的回调函数 PreOperationPostOperation。其中:

    • PreOperation:在句柄创建/复制发生之前调用,允许回调函数修改访问权限,直接阻止访问行为。
    • PostOperation:在句柄创建/复制完成之后调用,不再允许修改访问权限。

    注意这两个回调函数至少需要填写一个。

回调函数注册的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <ntddk.h>
#include <ntstrsafe.h>

typedef struct _KLDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
ULONG __Undefined1;
ULONG __Undefined2;
ULONG __Undefined3;
ULONG NonPagedDebugInfo;
ULONG DllBase;
ULONG EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
// ...(后续字段省略)
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;

// 回调句柄,全局保存用于卸载时注销
static PVOID g_ObCallbackHandle = NULL;

///////////////////////////////////////////////////////////////////////////////
// 驱动卸载函数:注销回调
///////////////////////////////////////////////////////////////////////////////
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);

if (g_ObCallbackHandle) {
ObUnRegisterCallbacks(g_ObCallbackHandle);
g_ObCallbackHandle = NULL;
DbgPrint("Ob callback unregistered.\n");
}

DbgPrint("Driver unloaded.\n");
}

///////////////////////////////////////////////////////////////////////////////
// 驱动入口:注册回调函数
///////////////////////////////////////////////////////////////////////////////
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);

PKLDR_DATA_TABLE_ENTRY DataTableEntry;

// 绕过驱动签名检测
DataTableEntry = (PKLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
DataTableEntry->Flags |= 0x20;

OB_CALLBACK_REGISTRATION callbackReg = { 0 };
OB_OPERATION_REGISTRATION opReg = { 0 };
UNICODE_STRING altitude = RTL_CONSTANT_STRING(L"320000.MyProcessProtector");

DriverObject->DriverUnload = DriverUnload;

// 填写操作注册结构(进程对象 + 创建句柄操作)
opReg.ObjectType = *PsProcessType;
opReg.Operations = OB_OPERATION_HANDLE_CREATE;
opReg.PreOperation = PreProcessHandleCreateCallback;
opReg.PostOperation = PostProcessHandleCreateCallback;

// 填写回调注册结构
callbackReg.Version = ObGetFilterVersion(); // 推荐使用动态版本
callbackReg.OperationRegistrationCount = 1;
callbackReg.Altitude = altitude;
callbackReg.RegistrationContext = NULL;
callbackReg.OperationRegistration = &opReg;

// 注册回调
NTSTATUS status = ObRegisterCallbacks(&callbackReg, &g_ObCallbackHandle);
if (!NT_SUCCESS(status)) {
DbgPrint("ObRegisterCallbacks failed: 0x%08X\n", status);
return status;
}

DbgPrint("Ob callback registered successfully.\n");
return STATUS_SUCCESS;
}

预操作回调函数

预操作回调函数是 Windows 内核在执行某些对象访问操作(如进程、线程、注册表键的句柄创建或复制)之前,调用驱动提供的拦截处理函数。预操作回调函数类型声明如下,该函数会在,在对象的句柄尚未创建之前调用。

1
2
3
4
5
6
7
8
9
10
11
///////////////////////////////////////////////////////////////////////////////
// 函数类型: POB_PRE_OPERATION_CALLBACK
// 用途: 定义预操作回调函数的函数指针类型。
// 当进程尝试创建或复制对象句柄时(如 ZwOpenProcess),系统将在句柄创建前调用此函数,
// 驱动可根据访问目标、权限请求决定是否修改权限或拒绝访问。
///////////////////////////////////////////////////////////////////////////////
typedef OB_PREOP_CALLBACK_STATUS
(*POB_PRE_OPERATION_CALLBACK)(
_In_ PVOID RegistrationContext, // 注册时提供的上下文指针,驱动可用于传递状态或配置
_Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation // 包含此次对象访问操作的详细信息(如下结构)
);

注意

预操作回调函数只能返回 OB_PREOP_SUCCESS

1
2
3
typedef enum _OB_PREOP_CALLBACK_STATUS {
OB_PREOP_SUCCESS
} OB_PREOP_CALLBACK_STATUS, *POB_PREOP_CALLBACK_STATUS;

其中 RegistrationContext 参数来自于注册回调函数时 OB_CALLBACK_REGISTRATION 结构体的 RegistrationContext 成员,用于传递用户自定义的参数。

另外 OperationInformation 参数存放了发生回调时保存的信息,该参数对应的 OB_PRE_OPERATION_INFORMATION 结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
///////////////////////////////////////////////////////////////////////////////
// 结构体: OB_PRE_OPERATION_INFORMATION
// 用途: 描述一次“预操作”回调上下文信息,用于分析当前对象访问行为。
// 提供对象类型、目标对象、访问权限等关键参数,供驱动进行控制。
///////////////////////////////////////////////////////////////////////////////
typedef struct _OB_PRE_OPERATION_INFORMATION {
//
// 当前操作类型:
// - OB_OPERATION_HANDLE_CREATE(0x1):请求创建对象句柄
// - OB_OPERATION_HANDLE_DUPLICATE(0x2):请求复制对象句柄
//
_In_ OB_OPERATION Operation;

union {
_In_ ULONG Flags; // 位标志字段,当前仅定义了 KernelHandle 位

struct {
_In_ ULONG KernelHandle : 1; // 若为 1,表示此操作为内核句柄操作,驱动不可干预
_In_ ULONG Reserved : 31;// 保留,必须忽略
};
};

//
// 被访问的对象指针,例如 PEPROCESS(进程)、PETHREAD(线程)、PCM_KEY_BODY(注册表键)等
//
_In_ PVOID Object;

//
// 被访问对象的类型指针,用于判断当前对象属于哪类(如 PsProcessType)
//
_In_ POBJECT_TYPE ObjectType;

//
// 用户可读写字段,用于驱动在 PreOperation 中设置自定义上下文,
// 后续可在 PostOperation 中通过该字段获取(用于跨阶段共享状态)
//
_Out_ PVOID CallContext;

//
// 包含访问操作的参数信息(联合体,依操作类型不同而异)
//
_In_ POB_PRE_OPERATION_PARAMETERS Parameters;

} OB_PRE_OPERATION_INFORMATION, *POB_PRE_OPERATION_INFORMATION;

OB_PRE_OPERATION_INFORMATION 结构体主要描述了事件类型内核对象相关信息。

由于事件分为 CREATEDUPLICATE 两种,因此事件参数字段 Parameters 是一个联合体 OB_PRE_OPERATION_PARAMETERS

1
2
3
4
5
6
7
8
9
///////////////////////////////////////////////////////////////////////////////
// 联合体: OB_PRE_OPERATION_PARAMETERS
// 用途: 根据当前操作类型(CREATE / DUPLICATE)提供不同的访问参数结构。
// 驱动在回调中通过 Operation 字段判断后选择访问对应成员。
///////////////////////////////////////////////////////////////////////////////
typedef union _OB_PRE_OPERATION_PARAMETERS {
OB_PRE_CREATE_HANDLE_INFORMATION CreateHandleInformation; // 用于 OB_OPERATION_HANDLE_CREATE
OB_PRE_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation; // 用于 OB_OPERATION_HANDLE_DUPLICATE
} OB_PRE_OPERATION_PARAMETERS, *POB_PRE_OPERATION_PARAMETERS;

CREATE 事件的参数是通过 OB_PRE_CREATE_HANDLE_INFORMATION 结构体描述,该结构体定义如下:

1
2
3
4
5
6
7
8
9
///////////////////////////////////////////////////////////////////////////////
// 结构体: OB_PRE_CREATE_HANDLE_INFORMATION
// 用途: 当进程请求创建某对象(如进程、线程、注册表)的句柄时,此结构提供访问权限信息。
// 驱动可通过修改 DesiredAccess 字段来控制最终访问权限。
///////////////////////////////////////////////////////////////////////////////
typedef struct _OB_PRE_CREATE_HANDLE_INFORMATION {
_Inout_ ACCESS_MASK DesiredAccess; // 可修改:最终授予用户的访问权限
_In_ ACCESS_MASK OriginalDesiredAccess; // 只读:用户原始请求的访问权限
} OB_PRE_CREATE_HANDLE_INFORMATION, *POB_PRE_CREATE_HANDLE_INFORMATION;

由于是在创建句柄前的回调,因此此时句柄还未创建,也就不会在参数中传入。

这里参数主要传入的是用户打开句柄时的请求权限, DesiredAccessOriginalDesiredAccess 的值传入的是一样的,但是我们只能通过修改 DesiredAccess 来设置最终授予用户的访问权限

Windows 内核之所以在 OB_PRE_CREATE_HANDLE_INFORMATION 中同时提供 OriginalDesiredAccess(只读)和 DesiredAccess(可修改),是为了 “保护请求意图” 与 “允许干预授权” 分离 —— 这是一种安全审计 + 控制分层设计理念。

例如下面这个示例代码可以给指定的进程句柄降权,从而达到了保护进程的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 要保护的目标进程 PID(可修改)
static HANDLE g_ProtectedPid = (HANDLE)1234;

///////////////////////////////////////////////////////////////////////////////
// PreOperation 回调函数:用于检查并修改访问权限
///////////////////////////////////////////////////////////////////////////////
OB_PREOP_CALLBACK_STATUS
PreProcessHandleCreateCallback(
_Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation
)
{
// 只处理句柄创建操作
if (OperationInformation->Operation != OB_OPERATION_HANDLE_CREATE)
return OB_PREOP_SUCCESS;

// 确保是进程对象
if (OperationInformation->ObjectType != *PsProcessType)
return OB_PREOP_SUCCESS;

// 获取目标进程 PID
PEPROCESS targetProcess = (PEPROCESS)OperationInformation->Object;
HANDLE pid = PsGetProcessId(targetProcess);

if (pid == g_ProtectedPid) {
ACCESS_MASK* pAccess = &OperationInformation->Parameters
->CreateHandleInformation.DesiredAccess;

// 剥除敏感权限
*pAccess &= ~(PROCESS_TERMINATE | PROCESS_VM_WRITE | PROCESS_VM_READ);

DbgPrint("Protected PID %u: access 0x%08X denied sensitive permissions\n",
(ULONG)(ULONG_PTR)pid, *pAccess);
}

return OB_PREOP_SUCCESS;
}

另外我们还可以通过调整 Altitude 让我们的对象回调函数注册到进程保护模块的对象回调函数之后,这样我们就可以恢复被降权句柄的权限。

DUPLICATE 事件由于是进行句柄复制,因此会同时传递句柄权限源进程目标进程

1
2
3
4
5
6
7
8
9
10
11
///////////////////////////////////////////////////////////////////////////////
// 结构体: OB_PRE_DUPLICATE_HANDLE_INFORMATION
// 用途: 当进程请求复制某对象句柄到目标进程时(如 DuplicateHandle),
// 此结构提供权限与进程上下文信息,驱动可进行审查或限制。
///////////////////////////////////////////////////////////////////////////////
typedef struct _OB_PRE_DUPLICATE_HANDLE_INFORMATION {
_Inout_ ACCESS_MASK DesiredAccess; // 可修改:目标进程最终获得的访问权限
_In_ ACCESS_MASK OriginalDesiredAccess; // 只读:原始复制请求的权限
_In_ PVOID SourceProcess; // 源进程的 EPROCESS 指针(原始句柄所有者)
_In_ PVOID TargetProcess; // 目标进程的 EPROCESS 指针(接收者)
} OB_PRE_DUPLICATE_HANDLE_INFORMATION, *POB_PRE_DUPLICATE_HANDLE_INFORMATION;

这里句柄权限同样分为 DesiredAccessOriginalDesiredAccess 两种。

后操作回调函数

后操作回调函数,是在对象访问操作(如创建或复制句柄)完成之后,由 Windows 内核自动调用的一个函数,用于通知驱动“这次访问操作已经完成”,供驱动执行观察、记录、收尾处理等任务。该函数类型 POB_POST_OPERATION_CALLBACK 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
///////////////////////////////////////////////////////////////////////////////
// 类型定义: POB_POST_OPERATION_CALLBACK
// 用途: 定义 Ob 对象回调机制中的“后操作回调函数”类型(PostOperation Callback)。
// 当对象句柄创建或复制操作完成后,系统调用该函数以通知驱动执行后续处理。
//
// 特点:
// - 回调在对象句柄操作完成后被调用(句柄已成功或失败创建/复制)
// - 不允许修改访问权限、访问控制等核心参数
// - 通常用于记录日志、行为审计、清理上下文或更新统计信息等
//
// 注册方式:
// 在 OB_OPERATION_REGISTRATION 中设置 PostOperation 成员指向此函数
//
// 注意:
// - 此函数无法阻止访问操作(如拒绝句柄创建),仅用于观察结果
// - 参数中的 RegistrationContext 来源于 OB_CALLBACK_REGISTRATION 中注册时提供的指针
// - 参数 OperationInformation 包含操作类型、对象类型、返回状态、上下文等信息
///////////////////////////////////////////////////////////////////////////////
typedef VOID
(*POB_POST_OPERATION_CALLBACK)(
_In_ PVOID RegistrationContext, // 驱动注册时提供的上下文指针,用于传递状态或策略配置
_In_ POB_POST_OPERATION_INFORMATION OperationInformation // 本次对象访问操作的结果信息结构体(见对应结构定义)
);

其中 OB_POST_OPERATION_INFORMATION 与前面的 OB_PRE_OPERATION_INFORMATION 结构完全一致,只不过 Parameters 的类型由 POB_PRE_OPERATION_PARAMETERS 变成了 POB_POST_OPERATION_PARAMETERS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _OB_POST_OPERATION_INFORMATION {
_In_ OB_OPERATION Operation;
union {
_In_ ULONG Flags;
struct {
_In_ ULONG KernelHandle:1;
_In_ ULONG Reserved:31;
};
};
_In_ PVOID Object;
_In_ POBJECT_TYPE ObjectType;
_In_ PVOID CallContext;
_In_ NTSTATUS ReturnStatus;
_In_ POB_POST_OPERATION_PARAMETERS Parameters;
} OB_POST_OPERATION_INFORMATION,*POB_POST_OPERATION_INFORMATION;

而由于后操作回调函数不修改权限,因此 OB_POST_OPERATION_PARAMETERS 中只有 GrantedAccess,并且对于 DUPLICATE 事件也不提供双方的进程对象地址。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _OB_POST_CREATE_HANDLE_INFORMATION {
_In_ ACCESS_MASK GrantedAccess;
} OB_POST_CREATE_HANDLE_INFORMATION, *POB_POST_CREATE_HANDLE_INFORMATION;

typedef struct _OB_POST_DUPLICATE_HANDLE_INFORMATION {
_In_ ACCESS_MASK GrantedAccess;
} OB_POST_DUPLICATE_HANDLE_INFORMATION, * POB_POST_DUPLICATE_HANDLE_INFORMATION;

typedef union _OB_POST_OPERATION_PARAMETERS {
_In_ OB_POST_CREATE_HANDLE_INFORMATION CreateHandleInformation;
_In_ OB_POST_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation;
} OB_POST_OPERATION_PARAMETERS, *POB_POST_OPERATION_PARAMETERS;

回调相关结构

对象回调的核心原理是在系统内核中维护一个“回调注册链表”,驱动程序通过提供的内核API注册自身的回调函数,并将其插入到对应对象类型(如进程或线程)的回调链表中。

每当内核执行对象操作(如打开句柄、复制句柄)时,都会主动遍历并调用这些注册的回调函数,进行拦截和控制。

OBJECT_TYPE

_OBJECT_TYPE 结构体描述了 某一种对象类型的整体特征,例如进程(PsProcessType)、线程(PsThreadType)、文件(IoFileObjectType)等。每个类型在内核中都有一个唯一的 _OBJECT_TYPE 实例,供该类型的所有对象共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 结构大小:0x88 字节
struct _OBJECT_TYPE
{
struct _LIST_ENTRY TypeList; // 0x00: 对象类型链表,用于链接所有 OBJECT_TYPE 实例
struct _UNICODE_STRING Name; // 0x08: 对象类型名称(如 "Process"、"Thread" 等)
VOID* DefaultObject; // 0x10: 默认对象实例(有些类型提供默认对象,例如 Token)
UCHAR Index; // 0x14: 对象类型索引(用于快速标识类型)
ULONG TotalNumberOfObjects; // 0x18: 当前系统中该类型对象的总数量
ULONG TotalNumberOfHandles; // 0x1C: 当前系统中指向该类型对象的总句柄数
ULONG HighWaterNumberOfObjects; // 0x20: 历史最高同时存在的该类型对象数量
ULONG HighWaterNumberOfHandles; // 0x24: 历史最高同时存在的该类型对象句柄数量

struct _OBJECT_TYPE_INITIALIZER TypeInfo; // 0x28: 类型初始化结构,定义该类型对象的行为(如回调、池分配方式等)

struct _EX_PUSH_LOCK TypeLock; // 0x78: 用于保护该类型对象的同步锁
ULONG Key; // 0x7C: 类型校验码或唯一标识符(某些版本中为保留字段)

struct _LIST_ENTRY CallbackList; // 0x80: 📌对象回调链表,用于支持注册的 ObRegisterCallbacks 回调
};

其中 CallbackList 是一个链表,存放我们注册的对象回调函数的存储结构 OB_CALLBACK_ENTRY

OBJECT_TYPE_INITIALIZER

_OBJECT_TYPE_INITIALIZER 定义了创建一个对象类型时需要提供的一组 初始化属性与行为接口。它是 _OBJECT_TYPETypeInfo 字段的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 0x50 bytes (sizeof)
struct _OBJECT_TYPE_INITIALIZER
{
USHORT Length; // 0x00: 结构体大小(用于版本兼容与校验)

union
{
UCHAR ObjectTypeFlags; // 0x02: 对象类型标志位字段(压缩位)
struct
{
UCHAR CaseInsensitive : 1; // [bit 0] 是否大小写不敏感(如符号链接对象)
UCHAR UnnamedObjectsOnly : 1; // [bit 1] 是否只允许匿名对象(如事件、信号量等)
UCHAR UseDefaultObject : 1; // [bit 2] 是否使用 DefaultObject 提供默认实例
UCHAR SecurityRequired : 1; // [bit 3] 创建时是否必须提供安全描述符
UCHAR MaintainHandleCount : 1; // [bit 4] 是否维护句柄引用计数
UCHAR MaintainTypeList : 1; // [bit 5] 是否加入全局对象类型链表
UCHAR SupportsObjectCallbacks : 1; // [bit 6] 📌是否支持对象回调(如 ObRegisterCallbacks)
UCHAR Reserved : 1; // [bit 7] 保留位
};
};

ULONG ObjectTypeCode; // 0x04: 对象类型代码(仅供内核识别)
ULONG InvalidAttributes; // 0x08: 无效的属性掩码(用于参数校验)

struct _GENERIC_MAPPING GenericMapping; // 0x0C: 用于权限转换的通用映射(GENERIC_READ 等)

ULONG ValidAccessMask; // 0x1C: 📌可用的访问权限位(ObCheckObjectAccess 用)
ULONG RetainAccess; // 0x20: 必须保留的访问权限位(不会被剥夺)

enum _POOL_TYPE PoolType; // 0x24: 对象默认使用的内存池类型

ULONG DefaultPagedPoolCharge; // 0x28: 分配分页池对象时的默认配额
ULONG DefaultNonPagedPoolCharge; // 0x2C: 分配非分页池对象时的默认配额

VOID (*DumpProcedure)( // 0x30: 调试器 !object 调用的对象转储回调
VOID* Object,
struct _OBJECT_DUMP_CONTROL* DumpControl
);

LONG (*OpenProcedure)( // 0x34: 对象被打开(NtOpenXXX)时的访问控制回调
enum _OB_OPEN_REASON OpenReason,
CHAR PreviousMode,
struct _EPROCESS* Process,
VOID* Object,
ULONG* GrantedAccess,
ULONG HandleCount
);

VOID (*CloseProcedure)( // 0x38: 对象句柄关闭(NtClose)时调用的回调
struct _EPROCESS* Process,
VOID* Object,
ULONG GrantedAccess,
ULONG HandleCount
);

VOID (*DeleteProcedure)( // 0x3C: 对象引用归零被删除时触发的清理回调
VOID* Object
);

LONG (*ParseProcedure)( // 0x40: 对象路径解析(如 NtOpenFile 解析路径)
VOID* ParsedObject,
VOID* RemainingPath,
struct _ACCESS_STATE* AccessState,
CHAR AccessMode,
ULONG Attributes,
struct _UNICODE_STRING* CompleteName,
struct _UNICODE_STRING* RemainingName,
VOID* Context,
struct _SECURITY_QUALITY_OF_SERVICE* SecurityQos,
VOID** NewObject
);

LONG (*SecurityProcedure)( // 0x44: 安全描述符读取/修改操作回调
VOID* Object,
enum _SECURITY_OPERATION_CODE OperationCode,
ULONG* SecurityInformation,
VOID* SecurityDescriptor,
ULONG* BufferLength,
VOID** NewSecurityDescriptor,
enum _POOL_TYPE PoolType,
struct _GENERIC_MAPPING* Mapping,
CHAR AccessMode
);

LONG (*QueryNameProcedure)( // 0x48: 查询对象名称信息的回调(如 NtQueryObject)
VOID* Object,
UCHAR HasAccess,
struct _OBJECT_NAME_INFORMATION* NameInfo,
ULONG NameInfoLength,
ULONG* ReturnLength,
CHAR AccessMode
);

UCHAR (*OkayToCloseProcedure)( // 0x4C: 判断是否允许关闭对象句柄(可用于保护特定句柄)
struct _EPROCESS* Process,
VOID* Object,
VOID* Handle,
CHAR PreviousMode
);
};

其中 SupportsObjectCallbacks 决定对象回调是否有效,如果将这个位置 0 则对象回调会失效。不过这个位被 PG 监控,不能长期修改。

另外 ValidAccessMask 表示的是可用的访问权限,如果这个位置 0 则该对象无法打开。例如内核中的 DbgkDebugObjectType 指向 DebugObject 的对象类型,如果把该类型的 ValidAccessMask 置 0 则会导致全局调试禁用。

OBJECT_TYPE_INITIALIZER 中还有一些列的回调函数,这些回调函数会针对不同内核对象的不同特性而指向不同的函数。

  • DumpProcedure :供调试工具(如 WinDbg 的 !object 扩展)用于打印对象内部状态,包括类型、引用计数、属性等。
  • OpenProcedure :当对象句柄打开时回调,允许拦截或修改访问请求。比如安全软件会在此处检查权限或拒绝打开。这个函数会在 ObpCreateHandle 内部处理时调用那个预操作(PreOperation)回调,以便对访问进行管理。
  • CloseProcedure :句柄关闭时执行,常用于资源释放、日志记录或实现引用计数清理机制。在用户调用 NtCloseCloseHandle 或句柄被回收时触发。
  • DeleteProcedure :当对象未再被引用且要销毁前调用,允许释放分配的结构体、内存或执行其他最终清理工作。通常在 ObDereferenceObject 导致引用计数为零,内核准备释放该对象时调用。
  • ParseProcedure :负责解析特定命名空间路径(如文件系统、设备路径等),并创建或引用对应对象。如从 \Device\... 路径进行 NtOpenFile 时,内核遍历命名空间,并在每个组件调用该回调函数。
  • SecurityProcedure :例如在 NtQuerySecurityObjectNtSetSecurityObject 等操作中,被调用以读取、安全验证、设置或重新编码安全描述符。通常在对象需要读取、设置或枚举安全信息时,在 ObpCreateHandleObQuerySecurityObject 等路径中触发。
  • QueryNameProcedure :当用户通过 API 查询对象名称(如文件名、进程名、设备路径)时,负责填充输出缓冲区。在 NtQueryObject(..., ObjectNameInformation) 时调用,用于提供准确名称信息。
  • OkayToCloseProcedure :在某些安全敏感或关键对象上使用,可拒绝某进程关闭句柄的请求,从而防止被篡改或持久化控制。在 NtClose 处理阶段,如果此回调返回 FALSE,系统将拒绝关闭句柄。

例如我们在调用 NtClose(hObject) 有如下过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
NtClose(hObject)

├─ ObpCloseHandle
│ │
│ ├─ 🎯 调用 ObpCloseHandleTableEntry
│ │ │
│ │ ├─ 🔍 调用 OkayToCloseProcedure(可拒绝句柄关闭)
│ │ │ ├─ 返回 TRUE → 继续
│ │ │ └─ 返回 FALSE → ❌ STATUS_HANDLE_NOT_CLOSABLE
│ │ │
│ │ └─ 🔢 调用 ObpDecrementHandleCount(递减句柄计数)
│ │ │
│ │ └─ 🔔 调用 CloseProcedure(不可拒绝,仅通知行为)
│ │ └─ 适用于日志、资源同步、Handle DB 清理等
│ │
│ └─ ObDereferenceObject
│ │
│ └─ ObfDereferenceObjectWithTag
│ │
│ └─ ObpRemoveObjectRoutine(对象引用为 0 时触发)
│ │
│ ├─ 🔐 调用 SecurityProcedure(若注册)
│ │ └─ 可校验、更新、释放安全描述符
│ │
│ └─ 💣 调用 DeleteProcedure(若注册)
│ └─ 最终释放资源、销毁对象

OB_CALLBACK_HANDLE

当我们调用 ObRegisterCallbacks 注册回调函数成功后会返回一个 RegistrationHandle 句柄。实际上 RegistrationHandle 指向的是 OB_CALLBACK_HANDLE(未公开)结构体。

这个结构体中保存了我们注册内核对象回调时的 OB_CALLBACK_REGISTRATION 结构体传递的信息,并且结构与 OB_CALLBACK_REGISTRATION 也非常相似。

1
2
3
4
5
6
7
8
9
10
11
12
//
// 对象回调注册结构体(即 ObRegisterCallbacks 返回的 RegistrationHandle 所指向的结构)
// 用于表示一个完整的注册上下文,包括注册的回调项、Altitude 等信息。
//
typedef struct _OB_CALLBACK_HANDLE {
USHORT Version; // 注册结构版本号,必须为 OB_FLT_REGISTRATION_VERSION
USHORT OperationRegistrationCount; // 实际成功注册的回调项数量(CallbackEntryArray 中的项数)
PVOID RegistrationContext; // 调用者定义的上下文,可在回调中访问
UNICODE_STRING Altitude; // 注册高度(Altitude),用于排序回调优先级(越高越先执行)
OB_CALLBACK_ENTRY CallbackEntryArray[1]; // 变长数组,用于存储所有回调项(按需分配内存)
} OB_CALLBACK_HANDLE, *POB_CALLBACK_HANDLE;

提示

ObRegisterCallbacks 在申请 OB_CALLBACK_HANDLE 结构体所需内存时,会在 OB_CALLBACK_HANDLE 的最后额外多申请一部分内存,用于存放 Altitude 中的字符串。

其中 CallbackEntryArray 是一个 OB_CALLBACK_ENTRY 数组,对应着 OB_CALLBACK_REGISTRATIONOperationRegistration 字段。里面保存在我们注册的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
//
// 单个对象操作回调项,挂入指定对象类型的 CallbackList 中
//
typedef struct _OB_CALLBACK_ENTRY {
LIST_ENTRY CallbackList; // 链表节点,挂入 ObjectType->CallbackList 链表
ULONG Operations; // 触发操作标志:如 OB_OPERATION_HANDLE_CREATE / DUPLICATE
ULONG Flags; // 状态标志:如 OB_CALLBACK_ACTIVE_FLAG
POB_CALLBACK_HANDLE CallbackHandle; // 所属回调句柄(OB_CALLBACK_HANDLE),用于回溯清理
POBJECT_TYPE ObjectType; // 注册目标对象类型(如 PsProcessType、PsThreadType)
POB_PRE_OPERATION_CALLBACK PreOperation; // 创建或复制前的预回调函数指针
POB_POST_OPERATION_CALLBACK PostOperation; // 创建或复制后的后回调函数指针
EX_RUNDOWN_REF RundownRef; // Rundown 保护,用于延迟释放 CallbackEntry
} OB_CALLBACK_ENTRY, *POB_CALLBACK_ENTRY;

这个结构通过 CallbackList 字段挂到 OBJECT_TYPECallbackList 链表上。

对象回调分析

对象回调注册

ObRegisterCallbacks 根据我们传入的 CallbackRegistration 结构构造填充一个 POB_CALLBACK_HANDLE 结构,并调用 ObpInsertCallbackByAltitude 函数将其中 CallbackEntryArray 数组中的所有的 POB_CALLBACK_ENTRY 按照 Altitude 顺序挂到 OBJECT_TYPECallbackList 链表上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
NTSTATUS NTAPI ObRegisterCallbacks(
POB_CALLBACK_REGISTRATION CallbackRegistration,
PVOID* RegistrationHandle
) {
USHORT RegistrationCount;
SIZE_T AllocationSize;
POB_CALLBACK_HANDLE CallbackHandle;
POB_CALLBACK_ENTRY CallbackEntry;
UNICODE_STRING* Altitude;
WCHAR* AltitudeBuffer;
NTSTATUS Status = STATUS_SUCCESS;
ULONG i;

// ------------------------------------------------------------
// [验证结构版本]
// ------------------------------------------------------------
if ((CallbackRegistration->Version & 0xFF00) != OB_FLT_REGISTRATION_VERSION)
return STATUS_INVALID_PARAMETER;

RegistrationCount = CallbackRegistration->OperationRegistrationCount;
if (RegistrationCount == 0)
return STATUS_INVALID_PARAMETER;

// ------------------------------------------------------------
// [计算所需内存大小并分配]
// ------------------------------------------------------------
AllocationSize =
FIELD_OFFSET(OB_CALLBACK_HANDLE, CallbackEntryArray) + // 固定部分
RegistrationCount * sizeof(OB_CALLBACK_ENTRY) + // 全部回调项
CallbackRegistration->Altitude.Length; // Altitude 字符串

CallbackHandle = (POB_CALLBACK_HANDLE)ExAllocatePoolWithTag(PagedPool, AllocationSize, 'ObCl');
if (!CallbackHandle)
return STATUS_INSUFFICIENT_RESOURCES;

RtlZeroMemory(CallbackHandle, AllocationSize);

CallbackHandle->Version = OB_FLT_REGISTRATION_VERSION;
CallbackHandle->RegistrationContext = CallbackRegistration->RegistrationContext;
CallbackHandle->OperationRegistrationCount = 0;

// ------------------------------------------------------------
// [拷贝 Altitude 字符串至结构尾部]
// ------------------------------------------------------------
AltitudeBuffer = (WCHAR*)((PUCHAR)CallbackHandle + AllocationSize - CallbackRegistration->Altitude.Length);
RtlCopyMemory(AltitudeBuffer, CallbackRegistration->Altitude.Buffer, CallbackRegistration->Altitude.Length);

Altitude = &CallbackHandle->Altitude;
Altitude->Length = CallbackRegistration->Altitude.Length;
Altitude->MaximumLength = Altitude->Length;
Altitude->Buffer = AltitudeBuffer;

// ------------------------------------------------------------
// [逐项注册回调]
// ------------------------------------------------------------
for (i = 0; i < RegistrationCount; ++i) {
const OB_OPERATION_REGISTRATION* OperationRegistration = &CallbackRegistration->OperationRegistration[i];
POBJECT_TYPE ObjectType = *OperationRegistration->ObjectType;

// 验证对象类型支持回调
if (!OperationRegistration->Operations || !ObjectType->TypeInfo.SupportsObjectCallbacks) {
Status = STATUS_INVALID_PARAMETER;
break;
}

// 验证回调函数合法性
if (OperationRegistration->PreOperation &&
!MmVerifyCallbackFunction((PVOID)OperationRegistration->PreOperation)) {
Status = STATUS_ACCESS_DENIED;
break;
}

if (OperationRegistration->PostOperation &&
!MmVerifyCallbackFunction((PVOID)OperationRegistration->PostOperation)) {
Status = STATUS_ACCESS_DENIED;
break;
}

// 填充当前回调项
CallbackEntry = &CallbackHandle->CallbackEntryArray[i];
CallbackEntry->Operations = OperationRegistration->Operations;
CallbackEntry->CallbackHandle = CallbackHandle;
CallbackEntry->ObjectType = ObjectType;
CallbackEntry->PreOperation = OperationRegistration->PreOperation;
CallbackEntry->PostOperation = OperationRegistration->PostOperation;
CallbackEntry->RundownRef = NULL;
InitializeListHead(&CallbackEntry->CallbackList);

Status = ObpInsertCallbackByAltitude(CallbackEntry, ObjectType);
if (!NT_SUCCESS(Status))
break;

CallbackHandle->OperationRegistrationCount++;
}

// ------------------------------------------------------------
// [失败时清理已插入回调]
// ------------------------------------------------------------
if (!NT_SUCCESS(Status)) {
for (i = 0; i < CallbackHandle->OperationRegistrationCount; ++i) {
POB_CALLBACK_ENTRY Entry = &CallbackHandle->CallbackEntryArray[i];
POBJECT_TYPE ObjectType = Entry->ObjectType;
EX_PUSH_LOCK* Lock = &ObjectType->TypeLock;

KeEnterCriticalRegion();
ExAcquirePushLockExclusive(Lock);
RemoveEntryList(&Entry->CallbackList);
ExReleasePushLockExclusive(Lock);
KeLeaveCriticalRegion();
}

ExFreePoolWithTag(CallbackHandle, 'ObCl');
} else {
// ------------------------------------------------------------
// [成功:标记所有回调为激活状态]
// ------------------------------------------------------------
for (i = 0; i < CallbackHandle->OperationRegistrationCount; ++i) {
CallbackHandle->CallbackEntryArray[i].Flags |= OB_CALLBACK_ACTIVE_FLAG;
}

*RegistrationHandle = CallbackHandle;
}

return Status;
}

ObpInsertCallbackByAltitude 函数则会根据 Altitude 找到合适的位置插入链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
NTSTATUS ObpInsertCallbackByAltitude(
POB_CALLBACK_ENTRY CallbackEntry,
POBJECT_TYPE ObjectType
) {
PLIST_ENTRY CallbackListHead;
PLIST_ENTRY Current;
UNICODE_STRING* Altitude;
NTSTATUS Status = STATUS_SUCCESS;

// ------------------------------------------------------------
// [进入关键区,获取对象类型的回调锁(PushLock 排他模式)]
// ------------------------------------------------------------
KeEnterCriticalRegion();
ExAcquirePushLockExclusive(&ObjectType->TypeLock);

CallbackListHead = &ObjectType->CallbackList;
Current = CallbackListHead->Flink;

// 获取插入项的 Altitude(优先级字符串)
Altitude = &CallbackEntry->CallbackHandle->Altitude;

// ------------------------------------------------------------
// [遍历对象类型的回调链表,按 Altitude 降序查找插入位置]
// ------------------------------------------------------------
while (Current != CallbackListHead) {
POB_CALLBACK_ENTRY Entry =
CONTAINING_RECORD(Current, OB_CALLBACK_ENTRY, CallbackList);

POB_CALLBACK_HANDLE Registration = Entry->CallbackHandle;

int CompareResult = RtlCompareAltitudes(
&Registration->Altitude,
Altitude
);

if (CompareResult <= 0) {
// Altitude 相同表示冲突
if (CompareResult == 0) {
Status = STATUS_FLT_INSTANCE_ALTITUDE_COLLISION;
}
break;
}

Current = Current->Flink;
}

// ------------------------------------------------------------
// [执行插入或因冲突返回错误]
// ------------------------------------------------------------
if (NT_SUCCESS(Status)) {
InsertTailList(Current->Blink, &CallbackEntry->CallbackList);
}

// ------------------------------------------------------------
// [释放锁并恢复 APC 状态]
// ------------------------------------------------------------
ExReleasePushLockExclusive(&ObjectType->TypeLock);
KeLeaveCriticalRegion();

return Status;
}
  • Title: windows 内核回调
  • Author: sky123
  • Created at : 2022-09-28 11:45:14
  • Updated at : 2025-07-08 21:53:03
  • Link: https://skyi23.github.io/2022/09/28/windows 内核回调/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments